﻿//-----------------------------------------------------------------------
// <copyright file="Synth.cs" company="Open Source">
//     This file is released under the Microsoft Reciprocal License (Ms-RL)
//     http://www.codeplex.com/wiidrumsynth/license
// </copyright>
//-----------------------------------------------------------------------

namespace MidiSynthLib
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.Text;
    using System.Threading;
    using Sanford.Multimedia.Midi;

    /// <summary>
    /// The MIDI Synth which will play instances of <see cref="MidiNote"/>
    /// </summary>
    public class Synth : IDisposable
    {
        #region Private Variables
        /// <summary>
        /// The MIDI output device which will be used.
        /// </summary>
        private OutputDevice outDevice;
        
        /// <summary>
        /// The MIDI channel which notes will be played on.
        /// </summary>
        private int midiChannel;
        
        /// <summary>
        /// The MIDI program which will be used to play notes.
        /// </summary>
        private int midiProgram;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="Synth"/> class.
        /// </summary>
        public Synth()
            : this(0, 0)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Synth"/> class.
        /// </summary>
        /// <param name="midiChannel">The midi channel.</param>
        /// <param name="midiProgram">The midi program.</param>
        public Synth(int midiChannel, int midiProgram)
        {
            if (OutputDevice.DeviceCount == 0)
            {
                throw new MidiSynthException("No output devices were found!");
            }
            else
            {
                this.outDevice = new OutputDevice(0);
                this.midiChannel = midiChannel;
                this.midiProgram = midiProgram;
            }
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the output device id.
        /// </summary>
        /// <value>The output device id.</value>
        public int OutputDeviceId
        {
            get 
            { 
                return this.outDevice.DeviceID; 
            }

            set
            {
                this.outDevice.Close();
                this.outDevice = new OutputDevice(value);
            }
        }

        /// <summary>
        /// Gets or sets the midi program.
        /// </summary>
        /// <value>The midi program.</value>
        public int MidiProgram
        {
            get
            {
                return this.midiProgram;
            }

            set
            {
                if (this.SetProgram(value))
                {
                    this.midiProgram = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the midi instrument.
        /// </summary>
        /// <value>The midi instrument.</value>
        public GeneralMidiInstrument MidiInstrument
        {
            get
            {
                return (GeneralMidiInstrument)this.midiProgram;
            }

            set
            {
                if (this.SetProgram((int)value))
                {
                    this.midiProgram = (int)value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the midi channel.
        /// </summary>
        /// <value>The midi channel.</value>
        public int MidiChannel
        {
            get
            {
                return this.midiChannel;
            }

            set
            {
                this.midiChannel = value;
                this.SetProgram(this.midiProgram);
            }
        }

        #endregion

        #region Public Methods
        /// <summary>
        /// Plays the given MIDI note at the given velocity.
        /// </summary>
        /// <param name="note">The note to be played</param>
        /// <param name="velocity">The velocity at which to play the note</param>
        public void PlayNote(int note, int velocity)
        {
#if DEBUG
            Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture) + " - Entering PlayNote");
#endif
            MidiNote midiNote = new MidiNote(note, velocity);
            ThreadPool.QueueUserWorkItem(this.InternalPlayNote, midiNote);
#if DEBUG
            Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture) + " - Leaving PlayNote");
#endif
        }

        #region IDisposable Members

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion
        #endregion

        #region Protected Methods
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <param name="disposing">Dispose of managed resources</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // free managed resources
                if (this.outDevice != null)
                {
                    this.outDevice.Dispose();
                    this.outDevice = null;
                }
            }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Sets the program.
        /// </summary>
        /// <param name="program">The program.</param>
        /// <returns>Was the program successfully set</returns>
        private bool SetProgram(int program)
        {
            bool success = false;
            if (this.outDevice != null)
            {
                this.outDevice.Send(new ChannelMessage(ChannelCommand.ProgramChange, this.midiChannel, program));
                success = true;
            }

            return success;
        }

        /// <summary>
        /// Internal function to play a MIDI note. This function is called by the threads in the ThreadPool.
        /// </summary>
        /// <param name="midiNote">The midi note.</param>
        private void InternalPlayNote(object midiNote)
        {
#if DEBUG
            Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture) + " - Entering InternalPlayNote");
#endif
            MidiNote note = (MidiNote)midiNote;
            if (this.outDevice != null)
            {
                this.outDevice.Send(new ChannelMessage(ChannelCommand.NoteOn, this.midiChannel, note.Note, note.Velocity));
                Thread.Sleep(250);
                this.outDevice.Send(new ChannelMessage(ChannelCommand.NoteOff, this.midiChannel, note.Note, 0));
            }
#if DEBUG
            Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture) + " - Leaving InternalPlayNote");
#endif
        }
        #endregion
    }
}
